// Copyright 2019-2025 PureStake Inc.
// This file is part of Moonbeam.

// Moonbeam is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// Moonbeam is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with Moonbeam.  If not, see <http://www.gnu.org/licenses/>.

#![allow(dead_code)]

use cumulus_primitives_parachain_inherent::ParachainInherentData;
use fp_evm::GenesisAccount;
use frame_support::{
	assert_ok,
	traits::{OnFinalize, OnInitialize},
};
pub use moonriver_runtime::{
	currency::MOVR, AccountId, AsyncBacking, AuthorInherent, Balance, Ethereum, InflationInfo,
	ParachainStaking, Range, Runtime, RuntimeCall, RuntimeEvent, System, TransactionConverter,
	UncheckedExtrinsic, HOURS,
};
use nimbus_primitives::{NimbusId, NIMBUS_ENGINE_ID};
use polkadot_parachain::primitives::HeadData;
use sp_consensus_slots::Slot;
use sp_core::{Encode, H160};
use sp_runtime::{traits::Dispatchable, BuildStorage, Digest, DigestItem, Perbill, Percent};

use cumulus_pallet_parachain_system::MessagingStateSnapshot;
use cumulus_primitives_core::relay_chain::{AbridgedHostConfiguration, AsyncBackingParams};
use cumulus_primitives_core::AbridgedHrmpChannel;
use fp_rpc::ConvertTransaction;
use moonriver_runtime::bridge_config::XcmOverPolkadotInstance;
use moonriver_runtime::{EvmForeignAssets, XcmWeightTrader};
use pallet_transaction_payment::Multiplier;
use std::collections::BTreeMap;
use xcm::latest::{InteriorLocation, Location};

pub fn existential_deposit() -> u128 {
	<Runtime as pallet_balances::Config>::ExistentialDeposit::get()
}

/// Returns mock AbridgedHostConfiguration for ParachainSystem tests
pub fn mock_abridged_host_config() -> AbridgedHostConfiguration {
	AbridgedHostConfiguration {
		max_code_size: 3_145_728,
		max_head_data_size: 20_480,
		max_upward_queue_count: 174_762,
		max_upward_queue_size: 1_048_576,
		max_upward_message_size: 65_531,
		max_upward_message_num_per_candidate: 16,
		hrmp_max_message_num_per_candidate: 10,
		validation_upgrade_cooldown: 6,
		validation_upgrade_delay: 6,
		async_backing_params: AsyncBackingParams {
			max_candidate_depth: 3,
			allowed_ancestry_len: 2,
		},
	}
}

// A valid signed Alice transfer.
pub const VALID_ETH_TX: &str =
	"02f86d8205018085174876e80085e8d4a5100082520894f24ff3a9cf04c71dbc94d0b566f7a27b9456\
	6cac8080c001a0e1094e1a52520a75c0255db96132076dd0f1263089f838bea548cbdbfc64a4d19f031c\
	92a8cb04e2d68d20a6158d542a07ac440cc8d07b6e36af02db046d92df";

// An invalid signed Alice transfer with a gas limit artifically set to 0.
pub const INVALID_ETH_TX: &str =
	"f86180843b9aca00809412cb274aad8251c875c0bf6872b67d9983e53fdd01801ca00e28ba2dd3c5a\
	3fd467d4afd7aefb4a34b373314fff470bb9db743a84d674a0aa06e5994f2d07eafe1c37b4ce5471ca\
	ecec29011f6f5bf0b1a552c55ea348df35f";

pub fn rpc_run_to_block(n: u32) {
	while System::block_number() < n {
		Ethereum::on_finalize(System::block_number());
		System::set_block_number(System::block_number() + 1);
		Ethereum::on_initialize(System::block_number());
	}
}

/// Utility function that advances the chain to the desired block number.
/// If an author is provided, that author information is injected to all the blocks in the meantime.
pub fn run_to_block(n: u32, author: Option<NimbusId>) {
	while System::block_number() < n {
		// Set the new block number and author
		match author {
			Some(ref author) => {
				let pre_digest = Digest {
					logs: vec![DigestItem::PreRuntime(NIMBUS_ENGINE_ID, author.encode())],
				};
				System::reset_events();
				System::initialize(
					&(System::block_number() + 1),
					&System::parent_hash(),
					&pre_digest,
				);
			}
			None => {
				System::set_block_number(System::block_number() + 1);
			}
		}

		increase_last_relay_slot_number(1);

		// Initialize the new block
		AuthorInherent::on_initialize(System::block_number());
		ParachainStaking::on_initialize(System::block_number());

		// Finalize the block
		ParachainStaking::on_finalize(System::block_number());
	}
}

pub fn last_event() -> RuntimeEvent {
	System::events().pop().expect("Event expected").event
}

// Helper function to give a simple evm context suitable for tests.
// We can remove this once https://github.com/rust-blockchain/evm/pull/35
// is in our dependency graph.
pub fn evm_test_context() -> fp_evm::Context {
	fp_evm::Context {
		address: Default::default(),
		caller: Default::default(),
		apparent_value: From::from(0),
	}
}

// Test struct with the purpose of initializing xcm assets
#[derive(Clone)]
pub struct XcmAssetInitialization {
	pub asset_id: u128,
	pub xcm_location: xcm::v5::Location,
	pub decimals: u8,
	pub name: &'static str,
	pub symbol: &'static str,
	pub balances: Vec<(AccountId, Balance)>,
}

pub struct ExtBuilder {
	// endowed accounts with balances
	balances: Vec<(AccountId, Balance)>,
	// [collator, amount]
	collators: Vec<(AccountId, Balance)>,
	// [delegator, collator, nomination_amount]
	delegations: Vec<(AccountId, AccountId, Balance, Percent)>,
	// per-round inflation config
	inflation: InflationInfo<Balance>,
	// AuthorId -> AccoutId mappings
	mappings: Vec<(NimbusId, AccountId)>,
	// Crowdloan fund
	crowdloan_fund: Balance,
	// Chain id
	chain_id: u64,
	// EVM genesis accounts
	evm_accounts: BTreeMap<H160, GenesisAccount>,
	// [assettype, metadata, Vec<Account, Balance,>, is_sufficient]
	xcm_assets: Vec<XcmAssetInitialization>,
	safe_xcm_version: Option<u32>,
	opened_bridges: Vec<(Location, InteriorLocation, Option<bp_moonbeam::LaneId>)>,
}

impl Default for ExtBuilder {
	fn default() -> ExtBuilder {
		ExtBuilder {
			balances: vec![],
			delegations: vec![],
			collators: vec![],
			inflation: InflationInfo {
				expect: Range {
					min: 100_000 * MOVR,
					ideal: 200_000 * MOVR,
					max: 500_000 * MOVR,
				},
				// not used
				annual: Range {
					min: Perbill::from_percent(50),
					ideal: Perbill::from_percent(50),
					max: Perbill::from_percent(50),
				},
				// unrealistically high parameterization, only for testing
				round: Range {
					min: Perbill::from_percent(5),
					ideal: Perbill::from_percent(5),
					max: Perbill::from_percent(5),
				},
			},
			mappings: vec![],
			crowdloan_fund: 0,
			chain_id: CHAIN_ID,
			evm_accounts: BTreeMap::new(),
			xcm_assets: vec![],
			safe_xcm_version: None,
			opened_bridges: vec![],
		}
	}
}

impl ExtBuilder {
	pub fn with_evm_accounts(mut self, accounts: BTreeMap<H160, GenesisAccount>) -> Self {
		self.evm_accounts = accounts;
		self
	}

	pub fn with_balances(mut self, balances: Vec<(AccountId, Balance)>) -> Self {
		self.balances = balances;
		self
	}

	pub fn with_collators(mut self, collators: Vec<(AccountId, Balance)>) -> Self {
		self.collators = collators;
		self
	}

	pub fn with_delegations(mut self, delegations: Vec<(AccountId, AccountId, Balance)>) -> Self {
		self.delegations = delegations
			.into_iter()
			.map(|d| (d.0, d.1, d.2, Percent::zero()))
			.collect();
		self
	}

	pub fn with_crowdloan_fund(mut self, crowdloan_fund: Balance) -> Self {
		self.crowdloan_fund = crowdloan_fund;
		self
	}

	pub fn with_mappings(mut self, mappings: Vec<(NimbusId, AccountId)>) -> Self {
		self.mappings = mappings;
		self
	}

	#[allow(dead_code)]
	pub fn with_inflation(mut self, inflation: InflationInfo<Balance>) -> Self {
		self.inflation = inflation;
		self
	}

	pub fn with_xcm_assets(mut self, xcm_assets: Vec<XcmAssetInitialization>) -> Self {
		self.xcm_assets = xcm_assets;
		self
	}

	pub fn with_safe_xcm_version(mut self, safe_xcm_version: u32) -> Self {
		self.safe_xcm_version = Some(safe_xcm_version);
		self
	}

	pub fn with_open_bridges(
		mut self,
		opened_bridges: Vec<(Location, InteriorLocation, Option<bp_moonbeam::LaneId>)>,
	) -> Self {
		self.opened_bridges = opened_bridges;
		self
	}

	pub fn build(self) -> sp_io::TestExternalities {
		let mut t = frame_system::GenesisConfig::<Runtime>::default()
			.build_storage()
			.unwrap();

		parachain_info::GenesisConfig::<Runtime> {
			parachain_id: <bp_moonriver::Moonriver as bp_runtime::Parachain>::PARACHAIN_ID.into(),
			_config: Default::default(),
		}
		.assimilate_storage(&mut t)
		.unwrap();

		pallet_balances::GenesisConfig::<Runtime> {
			balances: self.balances,
			dev_accounts: None,
		}
		.assimilate_storage(&mut t)
		.unwrap();

		pallet_parachain_staking::GenesisConfig::<Runtime> {
			candidates: self.collators,
			delegations: self.delegations,
			inflation_config: self.inflation,
			collator_commission: Perbill::from_percent(20),
			parachain_bond_reserve_percent: Percent::from_percent(30),
			blocks_per_round: 2 * HOURS,
			num_selected_candidates: 8,
		}
		.assimilate_storage(&mut t)
		.unwrap();

		pallet_author_mapping::GenesisConfig::<Runtime> {
			mappings: self.mappings,
		}
		.assimilate_storage(&mut t)
		.unwrap();

		let genesis_config = pallet_evm_chain_id::GenesisConfig::<Runtime> {
			chain_id: self.chain_id,
			..Default::default()
		};
		genesis_config.assimilate_storage(&mut t).unwrap();

		let genesis_config = pallet_evm::GenesisConfig::<Runtime> {
			accounts: self.evm_accounts,
			..Default::default()
		};
		genesis_config.assimilate_storage(&mut t).unwrap();

		let genesis_config = pallet_ethereum::GenesisConfig::<Runtime> {
			..Default::default()
		};
		genesis_config.assimilate_storage(&mut t).unwrap();

		let genesis_config = pallet_xcm::GenesisConfig::<Runtime> {
			safe_xcm_version: self.safe_xcm_version,
			..Default::default()
		};
		genesis_config.assimilate_storage(&mut t).unwrap();

		let genesis_config = pallet_transaction_payment::GenesisConfig::<Runtime> {
			multiplier: Multiplier::from(10u128),
			..Default::default()
		};
		genesis_config.assimilate_storage(&mut t).unwrap();

		let genesis_config = pallet_xcm_bridge::GenesisConfig::<Runtime, XcmOverPolkadotInstance> {
			opened_bridges: self.opened_bridges,
			_phantom: Default::default(),
		};
		genesis_config.assimilate_storage(&mut t).unwrap();

		let mut ext = sp_io::TestExternalities::new(t);
		let xcm_assets = self.xcm_assets.clone();
		ext.execute_with(|| {
			// Mock host configuration for ParachainSystem
			cumulus_pallet_parachain_system::HostConfiguration::<Runtime>::put(
				mock_abridged_host_config(),
			);

			// Mock hrmp egress_channels
			cumulus_pallet_parachain_system::RelevantMessagingState::<Runtime>::put(
				MessagingStateSnapshot {
					dmq_mqc_head: Default::default(),
					relay_dispatch_queue_remaining_capacity: Default::default(),
					ingress_channels: vec![],
					egress_channels: vec![(
						1_000.into(),
						AbridgedHrmpChannel {
							max_capacity: u32::MAX,
							max_total_size: u32::MAX,
							max_message_size: u32::MAX,
							msg_count: 0,
							total_size: 0,
							mqc_head: None,
						},
					)],
				},
			);

			// If any xcm assets specified, we register them here
			for xcm_asset_initialization in xcm_assets {
				let asset_id = xcm_asset_initialization.asset_id;
				EvmForeignAssets::create_foreign_asset(
					root_origin(),
					asset_id,
					xcm_asset_initialization.xcm_location.clone(),
					xcm_asset_initialization.decimals,
					xcm_asset_initialization
						.symbol
						.as_bytes()
						.to_vec()
						.try_into()
						.expect("too long"),
					xcm_asset_initialization
						.name
						.as_bytes()
						.to_vec()
						.try_into()
						.expect("too long"),
				)
				.expect("fail to create foreign asset");

				XcmWeightTrader::add_asset(
					root_origin(),
					xcm_asset_initialization.xcm_location,
					MOVR,
				)
				.expect("register evm native foreign asset as sufficient");

				for (account, balance) in xcm_asset_initialization.balances {
					if EvmForeignAssets::mint_into(asset_id, account, balance.into()).is_err() {
						panic!("fail to mint foreign asset");
					}
				}
			}
			System::set_block_number(1);
		});
		ext
	}
}

pub const CHAIN_ID: u64 = 1281;
pub const ALICE: [u8; 20] = [4u8; 20];
pub const ALICE_NIMBUS: [u8; 32] = [4u8; 32];
pub const BOB: [u8; 20] = [5u8; 20];
pub const CHARLIE: [u8; 20] = [6u8; 20];
pub const DAVE: [u8; 20] = [7u8; 20];
pub const EVM_CONTRACT: [u8; 20] = [8u8; 20];

pub fn origin_of(account_id: AccountId) -> <Runtime as frame_system::Config>::RuntimeOrigin {
	<Runtime as frame_system::Config>::RuntimeOrigin::signed(account_id)
}

pub fn inherent_origin() -> <Runtime as frame_system::Config>::RuntimeOrigin {
	<Runtime as frame_system::Config>::RuntimeOrigin::none()
}

pub fn root_origin() -> <Runtime as frame_system::Config>::RuntimeOrigin {
	<Runtime as frame_system::Config>::RuntimeOrigin::root()
}

/// Mock the inherent that sets validation data in ParachainSystem, which
/// contains the `relay_chain_block_number`, which is used in `author-filter` as a
/// source of randomness to filter valid authors at each block.
pub fn set_parachain_inherent_data() {
	use cumulus_primitives_core::PersistedValidationData;
	use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder;

	let author = AccountId::from(<pallet_evm::Pallet<Runtime>>::find_author());
	pallet_author_inherent::Author::<Runtime>::put(author);

	let mut relay_sproof = RelayStateSproofBuilder::default();
	relay_sproof.para_id = bp_moonriver::PARACHAIN_ID.into();
	relay_sproof.included_para_head = Some(HeadData(vec![1, 2, 3]));

	let additional_key_values = vec![];

	relay_sproof.additional_key_values = additional_key_values;

	let (relay_parent_storage_root, relay_chain_state) = relay_sproof.into_state_root_and_proof();

	let vfp = PersistedValidationData {
		relay_parent_number: 1u32,
		relay_parent_storage_root,
		..Default::default()
	};
	let parachain_inherent_data = ParachainInherentData {
		validation_data: vfp,
		relay_chain_state: relay_chain_state,
		downward_messages: Default::default(),
		horizontal_messages: Default::default(),
		collator_peer_id: Default::default(),
		relay_parent_descendants: Default::default(),
	};
	assert_ok!(RuntimeCall::ParachainSystem(
		cumulus_pallet_parachain_system::Call::<Runtime>::set_validation_data {
			data: parachain_inherent_data
		}
	)
	.dispatch(inherent_origin()));
}

pub fn unchecked_eth_tx(raw_hex_tx: &str) -> UncheckedExtrinsic {
	let converter = TransactionConverter;
	converter.convert_transaction(ethereum_transaction(raw_hex_tx))
}

pub fn ethereum_transaction(raw_hex_tx: &str) -> pallet_ethereum::Transaction {
	let bytes = hex::decode(raw_hex_tx).expect("Transaction bytes.");
	let transaction = ethereum::EnvelopedDecodable::decode(&bytes[..]);
	assert!(transaction.is_ok());
	transaction.unwrap()
}

pub(crate) fn increase_last_relay_slot_number(amount: u64) {
	let last_relay_slot = u64::from(AsyncBacking::slot_info().unwrap_or_default().0);
	frame_support::storage::unhashed::put(
		&frame_support::storage::storage_prefix(b"AsyncBacking", b"SlotInfo"),
		&((Slot::from(last_relay_slot + amount), 0)),
	);
}
